﻿using System;
using System.Diagnostics;
using System.IO;
using System.Collections.Generic;

namespace Sencha.Serialization.Bson
{
	[DebuggerDisplay("BsonWriter: readed={BytesReaded}")]
	public class BsonStreamReader : IJsonReader, IBsonReader
	{
		public static int MaxBufferSize = 1024 * 100; // 100kb

		[DebuggerDisplay("BsonToken: {Token}, value={Value}, pos={Position}")]
		private class BsonTokenInfo : IBsonValueInfo
		{
			private BsonStreamReader m_reader;
			private object m_value;
			private bool m_hasValue;
			private Dictionary<Type, IStrongBox> m_valueContainers;

			public BsonTokenInfo(BsonStreamReader reader)
			{
				if (reader == null)
					throw new ArgumentNullException("reader");

				m_valueContainers = new Dictionary<Type, IStrongBox>(32);
				m_reader = reader;
			}

			public bool HasValue { get { return m_hasValue; } }
			public object Raw
			{
				get
				{
					if (m_value is IStrongBox)
						return (m_value as IStrongBox).AsObject;
					else
						return m_value;
				}
				set
				{
					m_hasValue = true;
					m_value = value;
				}
			}
			public Type RawType
			{
				get
				{
					if (m_value is IStrongBox)
						return (m_value as IStrongBox).ValueType;
					else if (m_value != null)
						return m_value.GetType();
					else
						return null;
				}
			}
			public bool AsBoolean { get { return this.AsType<bool>(); } }
			public byte AsByte { get { if (m_value is IStrongBox) return (m_value as IStrongBox).AsByte; else return this.AsType<Byte>(); } }
			public short AsInt16 { get { if (m_value is IStrongBox) return (m_value as IStrongBox).AsInt16; else return this.AsType<Int16>(); } }
			public int AsInt32 { get { if (m_value is IStrongBox) return (m_value as IStrongBox).AsInt32; else return this.AsType<Int32>(); } }
			public long AsInt64 { get { if (m_value is IStrongBox) return (m_value as IStrongBox).AsInt64; else return this.AsType<Int64>(); } }
			public sbyte AsSByte { get { if (m_value is IStrongBox) return (m_value as IStrongBox).AsSByte; else return this.AsType<SByte>(); } }
			public ushort AsUInt16 { get { if (m_value is IStrongBox) return (m_value as IStrongBox).AsUInt16; else return this.AsType<UInt16>(); } }
			public uint AsUInt32 { get { if (m_value is IStrongBox) return (m_value as IStrongBox).AsUInt32; else return this.AsType<UInt32>(); } }
			public ulong AsUInt64 { get { if (m_value is IStrongBox) return (m_value as IStrongBox).AsUInt64; else return this.AsType<UInt64>(); } }
			public float AsSingle { get { if (m_value is IStrongBox) return (m_value as IStrongBox).AsSingle; else return this.AsType<Single>(); } }
			public double AsDouble { get { if (m_value is IStrongBox) return (m_value as IStrongBox).AsDouble; else return this.AsType<Double>(); } }
			public decimal AsDecimal { get { if (m_value is IStrongBox) return (m_value as IStrongBox).AsDecimal; else return this.AsType<Decimal>(); } }
			public DateTime AsDateTime { get { return this.AsType<DateTime>(); } }
			public BsonBinary AsBinary { get { return (BsonBinary)this.Raw; } }
			public BsonDateTime AsBsonDateTime { get { return this.AsType<BsonDateTime>(); } }
			public BsonJavaScript AsJavaScript { get { return (BsonJavaScript)this.Raw; } }
			public BsonJavaScriptWithScope AsJavaScriptWithScope { get { return (BsonJavaScriptWithScope)this.Raw; } }
			public BsonObjectId AsObjectId { get { return this.AsType<BsonObjectId>(); } }
			public BsonRegex AsRegex { get { return (BsonRegex)this.Raw; } }
			public BsonSymbol AsSymbol { get { return (BsonSymbol)this.Raw; } }
			public BsonTimestamp AsTimestamp { get { return this.AsType<BsonTimestamp>(); } }
			public string AsString
			{
				get
				{
					var raw = this.Raw;
					if (raw is string)
						return (string)raw;
					else
						return Convert.ToString(raw, m_reader.Context.Format);
				}
			}

			public long Position { get { return m_reader.m_bytesReaded; } }

			int IValueInfo.LineNumber { get { return 0; } }
			int IValueInfo.ColumnNumber { get { return (int)m_reader.m_bytesReaded; } }

			public T AsType<T>()
			{
				var raw = this.Raw;
				var value = m_value;

				if (value is IStrongBox)
					return (value as IStrongBox).Load<T>();
				else if (value is string)
					return TypeConvert.Convert<string, T>((string)value, m_reader.Context.Format);
				else
					return (T)value;
			}
#if NO_TYPE_REFS
			public void SetTypedValue<T>(T value)
			{
				var valueType = typeof(T);
				if (valueType.IsValueType)
				{
					var container = default(IStrongBox);
					if (!m_valueContainers.TryGetValue(valueType, out container))
					{
						container = (IStrongBox)Activator.CreateInstance(typeof(StrongBox<>).MakeGenericType(valueType));
						m_valueContainers[valueType] = container;
					}
					container.Store(value);
					this.Raw = container;
				}
				else
					this.Raw = (object)value;
			}
#else
			public void SetTypedValue(TypedReference value)
			{				
				var valueType = __reftype(value);
				if (valueType.IsValueType)
				{
					var container = default(IStrongBox);
					if (!m_valueContainers.TryGetValue(valueType, out container))
					{
						container = (IStrongBox)Activator.CreateInstance(typeof(StrongBox<>).MakeGenericType(valueType));
						m_valueContainers[valueType] = container;
					}
					container.Store(value);
					this.Raw = container;
				}
				else
					this.Raw = TypedReference.ToObject(value);
			}
#endif

			public void ClearValue()
			{
				m_value = null;
				m_hasValue = false;
			}

			public void CopyJsonTo(System.Text.StringBuilder stringBuilder)
			{
				if (stringBuilder == null)
					throw new ArgumentNullException("stringBuilder");

				this.CopyJsonTo(new JsonStringBuilderWriter(stringBuilder, m_reader.Context));
			}
			public void CopyJsonTo(System.IO.TextWriter writer)
			{
				if (writer == null)
					throw new ArgumentNullException("writer");

				this.CopyJsonTo(new JsonTextWriter(writer, m_reader.Context));
			}
			public void CopyJsonTo(System.IO.Stream stream)
			{
				if (stream == null)
					throw new ArgumentNullException("writer");

				this.CopyJsonTo(new JsonStreamWriter(stream, m_reader.Context));
			}
			public void CopyJsonTo(IJsonWriter writer)
			{
				throw new NotSupportedException();
			}
			public void CopyBsonTo(IBsonWriter writer)
			{
				if (writer == null)
					throw new ArgumentNullException("writer");

				writer.Write(m_reader.Token, this.Raw);
			}
		}
		[DebuggerDisplay("Document: {Type}, size={Size}b, digested={Digested}")]
		private struct Document
		{
			public int Size;
			public BsonToken Type;
			public BsonToken ClosingToken;
			public int Digested;

			public Document(BsonToken docType, int size)
			{
				this.Type = docType;
				this.ClosingToken = docType == BsonToken.BeginDocument ? BsonToken.EndOfDocument :
									docType == BsonToken.BeginArray ? BsonToken.EndOfArray :
									docType == BsonToken.JavaScriptWithScope ? BsonToken.EndOfDocument : BsonToken.EndOfStream;
				this.Size = size;
				this.Digested = 0;
			}
		}

		private BsonTokenInfo m_value;
		private BsonToken m_token;
		private MemoryStream m_buffer;
		private BinaryReader m_reader;
		private bool m_isEof;
		private IEnumerator<bool> m_readState;
		private int m_maxBufferSize = MaxBufferSize;
		private long m_bytesReaded;

		public ISerializationContext Context { get; private set; }

		public BsonStreamReader(Stream stream, ISerializationContext context)
		{
			if (stream == null)
				throw new ArgumentNullException("stream");
			if (context == null)
				throw new ArgumentNullException("context");

			var bytes = new byte[5 * 1024]; // 5kb
			m_buffer = new MemoryStream(bytes, 0, bytes.Length, true, true);
			m_value = new BsonTokenInfo(this);
			m_reader = new BinaryReader(stream, context.Encoding);
			this.Context = context;
		}

		JsonToken IJsonReader.Token { get { return this.MapBsonToken(this.Token); } }
		object IJsonReader.RawValue { get { return this.Value.Raw; } }
		IValueInfo IJsonReader.Value { get { return this.Value; } }
		long IJsonReader.CharactersReaded { get { return m_bytesReaded; } }

		public long BytesReaded { get { return m_bytesReaded; } }
		public BsonToken Token
		{
			get
			{
				if (m_token == BsonToken.None)
					this.NextToken();
				return m_token;
			}
		}
		public object RawValue { get { return m_value.Raw; } }
		public IBsonValueInfo Value
		{
			get
			{
				if (m_token == BsonToken.None)
					this.NextToken();
				return m_value;
			}
		}

		public bool NextToken()
		{
			if (m_readState == null)
				m_readState = ReadToken().GetEnumerator();

			try
			{
				if (!m_readState.MoveNext())
					return false;

				return m_readState.Current;
			}
			catch (EndOfStreamException)
			{
				m_token = BsonToken.EndOfStream;
				m_value.Raw = null;
				m_isEof = true;
				throw new Sencha.Serialization.Exceptions.UnexpectedEndOfStream(this);
			}
		}

		public void Reset()
		{
			m_bytesReaded = 0;
			if (!m_isEof)
				m_value = new BsonTokenInfo(this);
		}

		private IEnumerable<bool> ReadToken()
		{
			this.Context.DataContract.AddKnownType(typeof(BsonSymbol));
			this.Context.DataContract.AddKnownType(typeof(BsonBinary));
			this.Context.DataContract.AddKnownType(typeof(BsonDateTime));
			this.Context.DataContract.AddKnownType(typeof(BsonJavaScript));
			this.Context.DataContract.AddKnownType(typeof(BsonJavaScriptWithScope));
			this.Context.DataContract.AddKnownType(typeof(BsonMaxKey));
			this.Context.DataContract.AddKnownType(typeof(BsonMinKey));
			this.Context.DataContract.AddKnownType(typeof(BsonObjectId));
			this.Context.DataContract.AddKnownType(typeof(BsonRegex));
			this.Context.DataContract.AddKnownType(typeof(BsonTimestamp));
			this.Context.DataContract.AddKnownType(typeof(BsonUndefined));

			var stack = new Stack<Document>();

			if (m_token == BsonToken.None)
			{
				var documentSize = m_reader.ReadInt32();
				stack.Push(new Document(BsonToken.BeginDocument, documentSize));

				m_token = BsonToken.BeginDocument;
				yield return true;
			}

			var digested = 4;

			while (stack.Count > 0)
			{
				var tokenByte = m_reader.ReadByte(); digested += 1; m_bytesReaded += 1;
				var token = (BsonToken)tokenByte;

				if (token != BsonToken.None) // end of document
				{
					var name = this.ReadCString(ref digested);
					if (stack.Peek().Type == BsonToken.BeginDocument)
					{
						m_token = BsonToken.Member;
						m_value.Raw = name;
						yield return true;
					}
				}

				m_token = token;
				m_value.ClearValue();
				switch (token)
				{
					case BsonToken.Double:
						var doubleValue = m_reader.ReadDouble(); digested += 8; m_bytesReaded += 8;
#if NO_TYPE_REFS
						m_value.SetTypedValue(doubleValue);
#else
						m_value.SetTypedValue(__makeref(doubleValue));
#endif
						break;
					case BsonToken.Symbol:
					case BsonToken.String:
						m_value.Raw = this.ReadString(ref digested);
						break;
					case BsonToken.BeginDocument:
						AddDigested(stack, ref digested);
						var objSize = m_reader.ReadInt32(); digested += 4; m_bytesReaded += 4;
						stack.Push(new Document(token, objSize));
						break;
					case BsonToken.BeginArray:
						AddDigested(stack, ref digested);
						var arrSize = m_reader.ReadInt32(); digested += 4; m_bytesReaded += 4;
						stack.Push(new Document(token, arrSize));
						break;
					case BsonToken.Boolean:
						var boolValue = m_reader.ReadByte() != 0; digested += 1; m_bytesReaded += 1;
#if NO_TYPE_REFS
						m_value.SetTypedValue(boolValue);
#else
						m_value.SetTypedValue(__makeref(boolValue));
#endif
						break;
					case BsonToken.DateTime:
						var milliseconds = m_reader.ReadInt64(); digested += 8; m_bytesReaded += 8;
						var dateValue = new BsonDateTime(milliseconds);
#if NO_TYPE_REFS
						m_value.SetTypedValue(dateValue);
#else
						m_value.SetTypedValue(__makeref(dateValue));
#endif
						break;
					case BsonToken.Null:
						m_value.Raw = null;
						break;
					case BsonToken.Undefined:
						m_value.Raw = BsonUndefined.Value;
						break;
					case BsonToken.MinKey:
						m_value.Raw = BsonMinKey.Value;
						break;
					case BsonToken.MaxKey:
						m_value.Raw = BsonMaxKey.Value;
						break;
					case BsonToken.Regex:
						var pattern = this.ReadCString(ref digested);
						var optionsString = this.ReadCString(ref digested);
						m_value.Raw = new BsonRegex(pattern, optionsString);
						break;
					case BsonToken.Reference:
					case BsonToken.ObjectId:
						var objectId1 = m_reader.ReadInt32();
						var objectId2 = m_reader.ReadInt32();
						var objectId3 = m_reader.ReadInt32(); digested += 12; m_bytesReaded += 12;
						var objectId = new BsonObjectId(objectId1, objectId2, objectId3);
#if NO_TYPE_REFS
						m_value.SetTypedValue(objectId);
#else
						m_value.SetTypedValue(__makeref(objectId));
#endif
						break;
					case BsonToken.JavaScript:
						var code = this.ReadString(ref digested);
						m_value.Raw = new BsonJavaScript(code);
						break;
					case BsonToken.JavaScriptWithScope:
						var blockSize = m_reader.ReadInt32(); digested += 4; m_bytesReaded += 4;
						var scopedCode = this.ReadString(ref digested);
						m_token = BsonToken.JavaScriptWithScope;
						m_value.Raw = new BsonJavaScriptWithScope(scopedCode);
						AddDigested(stack, ref digested);

						var scopeSize = m_reader.ReadInt32(); digested += 4; m_bytesReaded += 4;
						stack.Push(new Document(BsonToken.BeginDocument, scopeSize));
						break;
					case BsonToken.Int32:
						var int32 = m_reader.ReadInt32(); digested += 4; m_bytesReaded += 4;
#if NO_TYPE_REFS
						m_value.SetTypedValue(int32);
#else
						m_value.SetTypedValue(__makeref(int32));
#endif
						break;
					case BsonToken.Timestamp:
						var timeStampValue = m_reader.ReadInt64(); digested += 8; m_bytesReaded += 8;
						var timeStamp = new BsonTimestamp(timeStampValue);
#if NO_TYPE_REFS
						m_value.SetTypedValue(timeStamp);
#else
						m_value.SetTypedValue(__makeref(timeStamp));
#endif
						break;
					case BsonToken.Int64:
						var int64 = m_reader.ReadInt64(); digested += 8; m_bytesReaded += 8;
#if NO_TYPE_REFS
						m_value.SetTypedValue(int64);
#else
						m_value.SetTypedValue(__makeref(int64));
#endif
						break;
					case BsonToken.Binary:
						var binaryLength = m_reader.ReadInt32(); digested += 4; m_bytesReaded += 4;
						if (binaryLength > m_maxBufferSize || binaryLength < 0) throw new Sencha.Serialization.Exceptions.BufferOvergrow(binaryLength, m_maxBufferSize, this);
						var subType = m_reader.ReadByte(); digested += 1; m_bytesReaded += 1;
						if (subType == 2)
						{
							binaryLength = m_reader.ReadInt32(); digested += 4; m_bytesReaded += 4;
							if (binaryLength > m_maxBufferSize || binaryLength < 0) throw new Sencha.Serialization.Exceptions.BufferOvergrow(binaryLength, m_maxBufferSize, this);
							m_value.Raw = new BsonBinary(subType, m_reader.ReadBytes(binaryLength)); digested += binaryLength; m_bytesReaded += binaryLength;
						}
						else
						{
							var genericBytes = m_reader.ReadBytes(binaryLength); digested += binaryLength; m_bytesReaded += binaryLength;
							if (binaryLength > m_maxBufferSize || binaryLength < 0) throw new Sencha.Serialization.Exceptions.BufferOvergrow(binaryLength, m_maxBufferSize, this);
							m_value.Raw = new BsonBinary(subType, genericBytes);
						}
						break;
					case BsonToken.None: // end of block
						AddDigested(stack, ref digested);
						if (IsFullyDigested(stack))
						{
							var doc = stack.Pop();

							digested += doc.Digested;
							if (stack.Count > 0) // if not top most document(has no upper doc)
								AddDigested(stack, ref digested); // then add my digested to upper doc digested

							m_token = doc.ClosingToken;
							m_value.Raw = null;
						}
						else
							throw new Sencha.Serialization.Exceptions.UnexpectedToken(this, JsonToken.Boolean, JsonToken.DateTime, JsonToken.Null, JsonToken.Number, JsonToken.StringLiteral);
						break;
				}

				yield return true;
			}

			m_isEof = true;
			yield break;
		}

		private string ReadCString(ref int digested)
		{
			// reset buffer
			m_buffer.SetLength(0);
			m_buffer.Position = 0;
			var readedByte = (byte)0;
			// read bytes to buffer until zero byte is met
			while ((readedByte = m_reader.ReadByte()) != 0)
			{
				if (m_buffer.Length > m_maxBufferSize)
					throw new Sencha.Serialization.Exceptions.BufferOvergrow((int)m_buffer.Length, m_maxBufferSize, this);
				m_buffer.WriteByte(readedByte);
			}
			// convert buffer to string
			var result = this.Context.Encoding.GetString(m_buffer.GetBuffer(), 0, (int)m_buffer.Length);
			// count buffer as digested
			digested += (int)m_buffer.Length + 1;
			m_bytesReaded += (int)m_buffer.Length + 1;

			return result;
		}
		private string ReadString(ref int digested)
		{
			// read string length
			var length = m_reader.ReadInt32();
			if (length > m_maxBufferSize || length < 0)
				throw new Sencha.Serialization.Exceptions.BufferOvergrow((int)length, m_maxBufferSize, this);
			// allocate buffer for string
			m_buffer.SetLength(length);
			var bytes = m_buffer.GetBuffer();
			// read bytes to buffer
			var readed = m_reader.BaseStream.Read(bytes, 0, length);
			if (readed != length)
				throw new EndOfStreamException();
			// convert buffer to string
			var result = this.Context.Encoding.GetString(bytes, 0, length - 1);
			// count as digested
			digested += 4 + length;
			m_bytesReaded += 4 + length;

			return result;
		}
		private void AddDigested(Stack<Document> stack, ref int digested)
		{
			var doc = stack.Pop();
			doc.Digested += digested;
			stack.Push(doc);
			digested = 0;
		}
		private bool IsFullyDigested(Stack<Document> stack)
		{
			var doc = stack.Peek();
			var nonTraversable = doc.Size == 0;
			var isDigested = doc.Digested == doc.Size;
			return nonTraversable || isDigested;
		}
		private JsonToken MapBsonToken(BsonToken token)
		{
			switch (token)
			{
				case BsonToken.None: return JsonToken.None;
				case BsonToken.Null: return JsonToken.Null;
				case BsonToken.BeginArray: return JsonToken.BeginArray;
				case BsonToken.Boolean: return JsonToken.Boolean;
				case BsonToken.Int32:
				case BsonToken.Int64:
				case BsonToken.Double: return JsonToken.Number;
				case BsonToken.BeginDocument: return JsonToken.BeginObject;
				case BsonToken.Member: return JsonToken.Member;
				case BsonToken.Symbol:
				case BsonToken.String: return JsonToken.StringLiteral;
				case BsonToken.EndOfStream: return JsonToken.EndOfStream;
				case BsonToken.EndOfArray: return JsonToken.EndOfArray;
				case BsonToken.EndOfDocument: return JsonToken.EndOfObject;
				// specials
				case BsonToken.DateTime:
				case BsonToken.Timestamp:
				case BsonToken.Binary:
				case BsonToken.Undefined:
				case BsonToken.MinKey:
				case BsonToken.MaxKey:
				case BsonToken.JavaScript:
				case BsonToken.ObjectId:
				case BsonToken.Reference:
				case BsonToken.Regex:
				case BsonToken.JavaScriptWithScope: return JsonToken.BeginObject;
				default: throw new InvalidOperationException(string.Format("Can't convert BSON token to JSON token, unknown token value - '{0}'", this.Token));
			}
		}
		public bool IsEndOfStream()
		{
			return m_isEof;
		}

	}
}
